本文示例项目Github连接:https://github.com/liu-if-else/UnityStencilBufferUses
最近有两次被人问到stencil buffer的用法,回答的含糊其辞,这两天研究了下它并总结出此文。
文章目录
1,Stencil buffer在OpenGL/Unity 渲染管线中的角色
为什么叫stencil 模板
stencil是印刷工业中的版面模子,模子上抠出需要的图案,然后将模子盖在要被印刷的材质上,对洞涂或喷绘颜色。
如果将屏幕上所有像素想象成一串连续的0组成的矩形,那么stencil buffer的作用就是将某些0变为1,2,3 … 255,在每个pass中可以决定只渲染某个特定stencil值的像素并抛弃对其他非该值像素的操作,就像一块模板一样扣住了所有像素,并只对当前stencil值的洞洞进行喷绘。
stencil与depth
stencil buffer与depth buffer一样,都是缓冲区,存在于显存内的某一片区域中。据wikipedia上解释,目前的显卡架构中,stencil buffer与depth buffer是在一起的,比如在depth/stencil缓冲区某个32位的区域中,有24位记录着像素A的depth数据,紧接着8位记录着像素A的stencil数据。也许就是由于它们连接如此紧密,在stencil test中可以获取到Z test的结果。在Unity中新建一个RenderTexture也可以通过设定深度值的位数来选择开启/关闭stencil buffer。
stencil测试在管线中的位置与它的写入与读取
在OpenGL渲染管线中,在片段着色器fragment shader之后,Blending混融之前有三个测试操作环节: scissor test(unity好像用不了),Stencil Test和Z-Test。
在Stencil Test环节,可通过使用关键字Comp读stencil值并与Ref值进行比较,通过Keep,Zero,Incr…对stencil进行写入,所有关键字说明请看Unity官网:
Stencil {
//当前像素stencil值与0进行比较
Ref 0 //0-255
//测试条件:测试是否相等
Comp Equal //default:always
//如果测试通过对此stencil值进行的写入操作:保持当前stencil值
Pass keep //default:keep
//如果测试失败对此stencil值进行的写入操作:保持当前stencil值
Fail keep //default:keep
//如果深度测试失败对此stencil值进行的写入操作:循环递增
ZFail IncrWrap //default:keep
}
2,使用Stencil buffer进行描边
代码
Shader "Unlit/StentilOutline"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Stencil {
Ref 0 //0-255
Comp Equal //default:always
Pass IncrSat //default:keep
Fail keep //default:keep
ZFail keep //default:keep
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
//return fixed4(1,1,0,1);
return col;
}
ENDCG
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float4 normal: NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex=v.vertex+normalize(v.normal)*0.01f;
o.vertex = UnityObjectToClipPos(o.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return fixed4(1,1,1,1);
}
ENDCG
}
}
}
说明
Stencil {
Ref 0 //0-255
Comp Equal //default:always
Pass IncrSat //default:keep
Fail keep //default:keep
ZFail keep //default:keep
}
buffer的值在当前帧结束前是不清除的,所以它可以跨越不同的shader与pass。Stencil结构写在Subshader中,那么下面的所有pass中的stencil test都按此运行。理想环境下,第一个pass渲染前屏幕上所有像素的stencil值都是0,在该pass的fragment shader结束后,所有进行了渲染的像素都通过了Ref 0和Comp Equal的测试,并执行Pass IncrSat将stencil值加1。
...
o.vertex=v.vertex+normalize(v.normal)*0.01f;
...
第二个pass中,将顶点进行进行了放大。进行同样的stencil测试,上一个pass渲染过的像素stencil值已经变为1,无法通过Ref 0+Comp Equal测试,那么现在只会在放大后的既是stencil值仍然为0的区域进行渲染。
...
return fixed4(1,1,1,1);
...
对第二个pass通过测试的像素给予描边颜色。
效果
图1:使用StencilPerPassOutline.shader
图2:使用StencilOutline.shader
3,使用stencil buffer进行多边形填充
这个效果与Unity官网中介绍stencil的第一个example shader类似,通过stencil值对几何体交叉区域进行判定与渲染。
代码
Shader "Unlit/PolygonsBeta"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
CGINCLUDE
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
ENDCG
Pass
{
Stencil {
Ref 0 //0-255
Comp always //default:always
Pass IncrWrap //default:keep
Fail keep //default:keep
ZFail IncrWrap //default:keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return fixed4(0,0,0,0);
}
ENDCG
}
Pass
{
Stencil {
Ref 2 //0-255
Comp Equal //default:always
Pass keep //default:keep
Fail keep //default:keep
ZFail keep //default:keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return fixed4(0.2,0.2,0.2,1);
}
ENDCG
}
Pass
{
Stencil {
Ref 3 //0-255
Comp equal //default:always
Pass keep //default:keep
Fail keep //default:keep
ZFail keep //default:keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return fixed4(0.6,0.6,0.6,1);
}
ENDCG
}
Pass
{
Stencil {
Ref 4 //0-255
Comp equal //default:always
Pass keep //default:keep
Fail keep //default:keep
ZFail keep //default:keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return fixed4(1,1,1,1);
}
ENDCG
}
}
}
说明
第一个pass渲染一个几何体,不论任何情况都通过测试并对它所覆盖的像素区域stencil值加1,后三个pass分别只对stencil值为2,3,4的区域进行渲染。
效果
图3:使用PolygonsBeta.shader
图4:使用polygons.shader
上图是结合此shader与以前文章里的阿基米德螺旋线算法放飞想象力的结果-_-。感觉用这招做Logo潜力好大…
4,用stencil buffer进行反射区域限定
此用法主要是辅助一个反射shader,可以比较简单的模拟出一个镜面效果。
代码
Shader "Unlit/TwoPassReflection"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
Pass
{
Stencil {
Ref 1 //0-255
Comp Equal //default:always
Pass keep //default:keep
Fail keep //default:keep
ZFail keep //default:keep
}
ZTest Always
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 normal: NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
v.vertex.xyz=reflect(v.vertex.xyz,float3(-1.0f,0.0f,0.0f));
v.vertex.xyz=reflect(v.vertex.xyz,float3(0.0f,1.0f,0.0f));
v.vertex.x+=1.5f;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Shader "Unlit/Mirror"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry-1" }
LOD 100
Stencil {
Ref 0 //0-255
Comp always //default:always
Pass IncrSat //default:keep
Fail keep //default:keep
ZFail keep //default:keep
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return fixed4(0.2f,0.2f,0.2f,1.0f);
}
ENDCG
}
}
}
说明
在TwoPassReflection.shader中,第一个pass正常渲染模型,第二个pass对顶点进行了一个简单的反射,并将ZTest设为always,然后将一个quad放入本体和倒影之间,它的效果是这样的:
图5:使用TwoPassReflection.shader,无mirror.shader
倒影超出了想要的范围。解决这一问题,在quad上使用mirror.shader将quad覆盖的像素stencil值改为1,并在TwoPassReflection第二个pass中约定只在stencil值为1的区域中渲染。
效果
图6:quad使用mirror.shader
这里有个前提是mirror必须在倒影之前渲染以先将反射区域的stencil值标记好。
5,阴影体shadow volume阴影渲染
说明
shadow volume阴影体算法是将‘遮光体’遮挡光源后产生的阴影实例为一个几何体,对在该阴影几何体的渲染过程中找出应该渲染阴影效果的像素。
图7:圆柱阴影体
检测手段有几种,本案例shader采用的是Depth Fail,也叫Carmack’s reverse方法的思路。它的思想与步骤如下:
1,在一般物体渲染后,渲染阴影体,第一个pass cull front,渲染内侧,在stencil测试阶段如果发现深度测试失败,说明该像素在阴影体内部表面或阴影体外部表面与视角之间有发生遮挡,将该像素stencil值加1。
2,第二个pass cull back,渲染外侧,如果有深度测试失败,则说明该像素在阴影体外部表面与视角之间有发生遮挡,将该像素stencil值减1。
3,经过两个pass的stencil操作,只有在阴影体内部的物体且它遮挡住阴影体内部表面的部分的stencil值为1。对阴影体内stencil值为1的像素进行渲染。
本文中的shader只为展示stencil buffer在此技术中的角色,缺乏正确的阴影体网格或它的动态生成手段,粗暴的用Unity默认几何体中的圆柱体模拟一个阴影体,并且算法中也没有考虑被阴影覆盖的物体自身的阴影体的问题以及其他细节问题。
代码
Shader "Unlit/SV_DepthFailBeta"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry+1"} //在渲染所有阴影体内物体后再渲染阴影体
LOD 100
CGINCLUDE //三个pass内着色器内容相同
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return fixed4(0.3,0.3,0.3,1); //影子颜色
}
ENDCG
Pass
{
Cull Front //阴影体内侧像素Z测试失败,stencil值加1
Stencil {
Ref 0 //0-255
Comp always //default:always
Pass keep //default:keep
Fail keep //default:keep
ZFail IncrWrap //default:keep
}
ColorMask 0 //关闭color buffer写入
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
ENDCG
}
Pass
{
Cull Back //阴影体外侧像素Z测试失败,stencil值减1
Stencil {
Ref 0 //0-255
Comp always //default:always
Pass keep //default:keep
Fail keep //default:keep
ZFail DecrWrap //default:keep
}
ColorMask 0 //关闭color buffer写入
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
ENDCG
}
Pass
{
Cull Back //经过前两个pass,stencil值为1的值为在此阴影体内被阴影覆盖的像素
Stencil {
Ref 1 //0-255
Comp equal //default:always
Pass keep //default:keep
Fail keep //default:keep
ZFail keep //default:keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
ENDCG
}
}
}
效果
图8:使用SV_DepthFailBeta.shader
参考:
Shadow Volume–Depth Fail, wikipedia:
https://en.wikipedia.org/wiki/Shadow_volume#Depth_fail
Creating Reflections and Shadows Using Stencil, BuffersMark J. Kilgard:
https://www2.cs.duke.edu/courses/spring15/cps124/classwork/14_buffers/stencil.pdf
Stencil Shadow Volume, OGL:
http://ogldev.atspace.co.uk/www/tutorial40/tutorial40.html
ShaderLab: Stencil, Unity:
https://docs.unity3d.com/Manual/SL-Stencil.html
Simon F’s answer to topic of ‘Uses for Stencil Buffer’ :
https://computergraphics.stackexchange.com/questions/5046/uses-for-stencil-buffer
本文示例项目Github连接:
https://github.com/liu-if-else/UnityStencilBufferUses
本文英文版:
https://liu-if-else.github.io/stencil-buffer’s-uses-in-unity3d/
维护日志:
2020-8-16:review